热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

时效性|事实_SSO轻量级实现指南(原生Java实现):SSOServer部分

篇首语:本文由编程笔记#小编为大家整理,主要介绍了SSO轻量级实现指南(原生Java实现):SSOServer部分相关的知识,希望对你有一定的参考价值。OAut

篇首语:本文由编程笔记#小编为大家整理,主要介绍了SSO 轻量级实现指南(原生 Java 实现):SSO Server 部分相关的知识,希望对你有一定的参考价值。


OAuth 是当前单点登录(SSO)和用户授权的标准协议——现在就让我们一起动手撸一个 SSO 的实现吧!

源码在:



  • SSO 中心 https://gitee.com/sp42_admin/ajaxjs/tree/master/aj-sso
  • SSO Client 客户端 https://gitee.com/sp42_admin/ajaxjs/tree/master/aj-sso-client

我们开源的特色:


  • 轻量级,代码行数少
  • 除了 JVM 和 Spring 没啥依赖,尽量原生,基本没什么第三方引用库
  • 代码风格务求清晰、简洁易维护、干净

SSO Server 即 SSO 中心,负责统一用户认证的。另外有 SSO Client 部分,我们另起文章再讲。


SSO 与 OAuth 傻傻分不清?

开始之前先说说废话(之所以说废话的原因是,其实你可以无视这段概念性的介绍,直接开撸)。

OAuth 是 OAuth,OAuth 不单单为 SSO 服务的。OAuth 协议初衷是为了用户不用告诉第三方系统账号和密码就可以访问受限的资源,——可以成为 SSO 的通行协议这个想必原设计者都没有料到的。没有 OAuth 之前,SSO 老早就有,只是各家各法自行实现,总能达到单点登录的目的。也就是说,SSO 的协议不一定是 OAuth,而 OAuth 不一定服务于 SSO。

SSO 与 OAuth 都是紧扣“我是谁”之要义,即用户身份认证的问题,这也是核心的问题,所有关于用户一切的信息都应由 SSO 中心或 OAuth 资源服务器所把控。稍有出入的是 OAuth 认证服务器往往是与资源服务器在一起的,这个一起的意思可以是物理意义上的同一部机器。但 SSO 中心呢?一般简单、纯粹的得多,就是做用户认证的,——即使涉及用户权限的 SSO 中心,顶多也是功能性的、系统级的权限控制,而不是垂直的数据权限控制(资源的权限控制)。也就是说,SSO 中心不负责资源问题,而资源往往在客户端 Client 那边。总之,狭义的 OAuth 很可能是整个大系统中,对外服务的一个模块;而 SSO 中心则纯粹得多,通常独立部署,独立服务,只做好 SSO 一件事情。

在流程上,SSO 与 OAuth 也有显著不同,例如同意授权访问,典型的第三方登录是有这一步的(如下图所示),但 SSO 没有吧?


SSO 流程

SSO 流程如下(借图,来自这里)


用户登录/注销

登录 Login

当前是使用账号密码登录,未来也应该支持如微信、微博的第三方登录。登录的作于在于识别“我是谁”的目的,在 SSO 中心标识某某用户已经是在登录的状态,以实现“单点登录”。具体说,会产生关键的标识状态 Session 和浏览器 COOKIEs。Session 仍是记忆登录状态的重要信息,否则后面获取 Token 就无法进行(因为不知道哪个用户!)。

登录控制器 LoginController 源码在这里,关键的 Service 部分在这里。

登录成功或失败一般允许指 redirectUrl,但我们没有,因为当前这登录接口是跨域的,界面完全由客户端指定,所以客户端自己控制就好。不过感觉上登录界面放在 SSO 中心会安全一点吧,毕竟允许跨域了。


注销

当前注销只是清空 session 而已,但实际 SSO 复杂得多,理论上某个应用注销了,其他所有已登录的应用也有要同步注销。这部分暂且不表,待后面补充。


注册

注册分为用户注册和客户端注册。


用户注册

用户注册没什么好说的,常规流程的逻辑,参见源码。


客户端注册

接入的客户端有时也称“应用”。客户端模型如下面 SQL 所示。clientId 有时也称 appIdappKeyclientSecret 是密钥,但跟密码的意思没什么区别,肯定不能外泄出去。

CREATE TABLE `auth_client_details` (
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '主键 id,自增',
`name` VARCHAR(20) NOT NULL COMMENT '客户端名称' COLLATE 'utf8mb4_bin',
`content` VARCHAR(256) NULL DEFAULT NULL COMMENT '简介' COLLATE 'utf8mb4_bin',
`clientId` VARCHAR(100) NOT NULL COMMENT '接入的客户端ID' COLLATE 'utf8mb4_bin',
`clientSecret` VARCHAR(255) NOT NULL COMMENT '接入的客户端的密钥' COLLATE 'utf8mb4_bin',
`redirecUri` VARCHAR(1000) NULL DEFAULT NULL COMMENT '回调地址' COLLATE 'utf8mb4_bin',
`stat` TINYINT(4) NULL DEFAULT NULL COMMENT '数据字典:状态',
`uid` BIGINT(20) NULL DEFAULT NULL COMMENT '唯一 id,通过 uuid 生成不重复 id',
`extend` TEXT NULL DEFAULT NULL COMMENT '扩展 JSON 字段' COLLATE 'utf8mb4_bin',
`tenantId` INT(11) NULL DEFAULT NULL COMMENT '租户 id',
`creator` INT(11) NULL DEFAULT NULL COMMENT '创建者 id',
`createDate` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '也是注册时间',
`updateDate` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE
)
COMMENT='接入的客户端信息表'
COLLATE='utf8mb4_bin'

clientIdclientSecret 都是随机字符串生成的,详见下面创建 client_details 部分。

@RestController
@RequestMapping("/oauth")
public class OauthController implements SsoDAO
/**
* 注册需要接入的客户端信息
*
* @param client
* @return
*/

@PostMapping(value = "/clientRegister", produces = MediaType.APPLICATION_JSON_VALUE)
public String clientRegister(@RequestParam ClientDetails client)
if (!StringUtils.hasText(client.getName()))
throw new IllegalArgumentException("客户端的名称和回调地址不能为空");
String clientId = StrUtil.getRandomString(24);// 生成24位随机的 clientId
ClientDetails savedClientDetails = findClientDetailsByClientId(clientId);
// 生成的 clientId 必须是唯一的,尝试十次避免有重复的 clientId
for (int i &#61; 0; i < 10; i&#43;&#43;)
if (savedClientDetails &#61;&#61; null)
break;
else
clientId &#61; StrUtil.getRandomString(24);
savedClientDetails &#61; findClientDetailsByClientId(clientId);


client.setClientId(clientId);
client.setClientSecret(StrUtil.getRandomString(32));
// 保存到数据库
return ClientDetailDAO.create(client) &#61;&#61; null ? BaseController.jsonNoOk() : BaseController.jsonOk();

……


SSO 登录

你以为上面用户登录就完事了&#xff1f;只是完成了三分之一&#xff0c;完整的单点登录还有其余的 66.6666……% ——我们接着看。


获取授权码

为什么要获取授权码&#xff08;Authorization Code&#xff09;&#xff0c;不能直接返回 Token 吗&#xff1f;因水平所限我也不太清楚&#xff0c;好像为了安全性吧&#xff0c;好像 OAuth 有其他模式不用授权码的&#xff1f;我没去管了&#xff0c;反正最主流就是授权码模式。不懂得看官请琢磨上面的流程图&#xff0c;或者先去消化 OAuth 的机制。

获取授权码接口所需的参数参见 SsoController 控制器的方法&#xff0c;源码这里。

&#64;Autowired
private AuthorizationService authService;
/**
* 获取 Authorization Code
*
* &#64;param client_id 客户端 ID
* &#64;param redirect_uri 回调 URL
* &#64;param scope 权限范围
* &#64;param status 用于防止CSRF攻击&#xff08;非必填&#xff09;
* &#64;param req 请求对象
* &#64;return
*/

&#64;RequestMapping(value &#61; "/authorize_code", produces &#61; BaseController.JSON)
public Object authorize(&#64;RequestParam(required &#61; true) String client_id,
// &#64;formatter:off
&#64;RequestParam(required &#61; true) String redirect_uri,
&#64;RequestParam(required &#61; false) String scope,
&#64;RequestParam(required &#61; false) String status,
HttpServletRequest req)
// &#64;formatter:on
LOGGER.info("获取 Authorization Code");
User loginedUser &#61; null;
try
loginedUser &#61; UserUtils.getLoginedUser(req);
catch (Throwable e)
LOGGER.warning(e);
return SsoUtil.oauthError(ErrorCodeEnum.INVALID_CLIENT);

// 生成 Authorization Code
String authorizationCode &#61; authService.createAuthorizationCode(client_id, scope, loginedUser);
String params &#61; "?code&#61;" &#43; authorizationCode;
if (StringUtils.hasText(status))
params &#43;&#61; "&status&#61;" &#43; status;
return new ModelAndView("redirect:" &#43; redirect_uri &#43; params);

据此我们了解几个事实。


  • 只有用户登录了&#xff0c;才有对应的授权码。UserUtils.getLoginedUser(req); 这句从 Session 返回已登录的用户信息。
  • 用户哪个浏览器登录&#xff0c;就在哪个浏览器获取授权码&#xff0c;不然就是未登录状态。
  • 该接口只能前端调用
  • 该接口返回 HTTP 304 重定向&#xff0c;携带 code 参数&#xff08;就是授权码&#xff09;跳转到 redirect_uri。就是说该接口不会返回什么 JSON。

状态码有时效性&#xff0c;一般十分钟&#xff0c;而且是一次性的&#xff0c;用完了要销毁。


客户端接入 SSO 之第一步

从原理上讲&#xff0c;这也是客户端服务接入 SSO 的第一步&#xff08;当然我们会提供一个封装好的 SDK&#xff0c;对于调用者是屏蔽细节的&#xff09;。用户成功登录后&#xff0c;已在 SSO 中心留存有 COOKIEs 的登录信息&#xff0c;于是其他第三方应用可以访问 SSO 中心获取用户信息&#xff08;当然不是直接获取&#xff0c;而且先要获取授权码&#xff09;。

客户端可以通过授权码获取 AccessToken&#xff0c;然后再根据 AccessToken 获取用户信息&#xff0c;完成本地登录。总之我们提到了两次登录验证&#xff1a;第一次是用户身份验证&#xff08;用户凭用户名和密码可以登录&#xff09;&#xff1b;第二次是客户端认证&#xff08;客户端凭 id 和密钥再结合用户信息&#xff08;授权码&#xff09;去登录&#xff09;&#xff0c;这部分我们下面小结会详细讲。


生成授权码原理

进入 authService.createAuthorizationCode() 源码我们看看如何生成授权码。

/**
* 根据 clientId、scope 以及当前时间戳生成 AuthorizationCode&#xff08;有效期为10分钟&#xff09;
*
* &#64;param clientId 客户端ID
* &#64;param scope
* &#64;param user 用户信息
* &#64;return
*/

public String createAuthorizationCode(String clientId, String scope, User user)
if (!StringUtils.hasText(scope))
scope &#61; "DEFAULT_SCOPE";
// 1. 拼装待加密字符串&#xff08;clientId &#43; scope &#43; 当前精确到毫秒的时间戳&#xff09;
String str &#61; clientId &#43; scope &#43; String.valueOf(System.currentTimeMillis());
// 2. SHA1加密
String encryptedStr &#61; Digest.getSHA1(str);
int timeout &#61; ExpireEnum.AUTHORIZATION_CODE.getTime() * 60;
// 3.1 保存本次请求的授权范围
ExpireCache.CACHE.put(encryptedStr &#43; ":scope", scope, timeout);
// 3.2 保存本次请求所属的用户信息
ExpireCache.CACHE.put(encryptedStr &#43; ":user", user, timeout);
// 4. 返回Authorization Code
return encryptedStr;

主要是这么几步&#xff1a;1. 拼装待加密字符串&#xff08;clientId &#43; scope &#43; 当前精确到毫秒的时间戳&#xff09;&#xff1b;2. SHA1 加密&#xff1b;3. 保存到缓存&#xff08;不用保存在数据库&#xff09;。

带过期时间的缓存大家想到的是 Redis&#xff0c;但我这里用了 JVM 的缓存&#xff0c;就是自己写的 Map&#xff0c;无他&#xff0c;懒得部署 Redis 了……


客户端认证&#xff08;颁发 AccessToken&#xff09;

客户端认证的过程就是颁发 AccessToken。我们看看客户端认证的源码定义&#xff0c;需要哪些参数。

/**
* 通过 Authorization Code 获取 Access Token
*
* &#64;param client_id 客户端 id
* &#64;param client_secret 接入的客户端的密钥
* &#64;param code 前面获取的 Authorization Code
* &#64;param grant_type 授权方式
* &#64;param request 请求对象
* &#64;return
*/

&#64;RequestMapping("/authorize")
public String issue(&#64;RequestParam(required &#61; true) String client_id,
// &#64;formatter:off
&#64;RequestParam(required &#61; true) String client_secret,
&#64;RequestParam(required &#61; true) String code,
&#64;RequestParam(required &#61; true) String grant_type,
HttpServletRequest request)
// &#64;formatter:on
LOGGER.info("通过 Authorization Code 获取 Access Token");
// 校验授权方式
if (!GrantTypeEnum.AUTHORIZATION_CODE.getType().equals(grant_type))
return SsoUtil.oauthError(ErrorCodeEnum.UNSUPPORTED_GRANT_TYPE);
ClientDetails savedClientDetails &#61; findClientDetailsByClientId(client_id);
// 校验请求的客户端秘钥和已保存的秘钥是否匹配
if (!(savedClientDetails !&#61; null && savedClientDetails.getClientSecret().equals(client_secret)))
return SsoUtil.oauthError(ErrorCodeEnum.INVALID_CLIENT);
String scope &#61; ExpireCache.CACHE.get(code &#43; ":scope", String.class);
User user &#61; ExpireCache.CACHE.get(code &#43; ":user", User.class);
// 如果能够通过 Authorization Code 获取到对应的用户信息&#xff0c;则说明该 Authorization Code 有效
if (StringUtils.hasText(scope) && user !&#61; null)
// 过期时间
Long expiresIn &#61; LocalDateUtils.dayToSecond(ExpireEnum.ACCESS_TOKEN.getTime());
// 生成 Access Token
String accessTokenStr &#61; authService.createAccessToken(user, savedClientDetails, grant_type, scope, expiresIn);
// 查询已经插入到数据库的 Access Token
AccessToken authAccessToken &#61; AcessTokenDAO.setWhereQuery("accessToken", accessTokenStr).findOne();
// 生成 Refresh Token
String refreshTokenStr &#61; authService.createRefreshToken(user, authAccessToken);
IssueToken token &#61; new IssueToken(); // 返回数据
token.setAccess_token(authAccessToken.getAccessToken());
token.setRefresh_token(refreshTokenStr);
token.setExpires_in(expiresIn);
token.setScope(authAccessToken.getScope());
return JsonHelper.toJson(token);
else
return SsoUtil.oauthError(ErrorCodeEnum.INVALID_GRANT);

据此我们了解几个事实。


  • 该接口只能服务端调用。客户端密钥保存在服务端&#xff0c;不应暴露给前端。故所以认证客户端务必在服务端完成&#xff0c;即后台来通讯请求。
  • 进入该接口&#xff0c;要判断密钥是否正确
  • 授权码相当于获取缓存中的 key&#xff0c;value 就是用户信息
  • client 和 user 没问题之后&#xff0c;可以创建 AccessToken
  • AccessToken 保存到数据库。如果已有则再更新。
  • 还生成 RefreshToken&#xff0c;这将会后面讲
  • 这个 AccessToken 外表一堆字符串&#xff0c;实际蕴含什么意思呢&#xff1f;Token 不是密码但胜似密码&#xff0c;他内部包含了不仅用户信息还有客户端的信息&#xff0c;故 AccessToken &#61; 用户&#43;客户端&#xff08;应用&#xff09;的信息

至此就完成了登录了&#xff0c;进度……100%。

至于生成 Token 原理大家可以进入 Service 相关代码浏览&#xff0c;大致都是 SHA1 加密之类的&#xff0c;这里不再赘述。

实际设计中有两点“最佳实践”分享给大家。


  • 虽然有份“获取授权码”和“客户端认证”两个接口两个步骤&#xff0c;但前端一次请求就可以搞定了&#xff0c;这是在 SSO_Client 前端执行的。
  • 单纯返回 AccessToken 之外&#xff0c;最好还要返回用户的详细信息&#xff0c;不然又要前端请求多次。当然标准的 OAuth 没要求返回用户信息。不过目前我还去实现……有时间就搞

刷新 AccessToken-----> RefreshToken

一般 Token 时效性。


  • AccessToken&#xff0c;三十天
  • RefreshToken 365 日

当然&#xff0c;根据你的场景调整。

逻辑大同小异&#xff0c;我们直接贴代码。

/**
* 通过 Refresh Token 刷新 Access Token
*
* &#64;param refresh_token
* &#64;return
*/

&#64;RequestMapping(value &#61; "/refreshToken", produces &#61; MediaType.APPLICATION_JSON_VALUE)
public String refreshToken(&#64;RequestParam(required &#61; true) String refresh_token)
LOGGER.info("通过 Refresh Token 刷新 Access Token");
RefreshToken authRefreshToken &#61; RefreshTokenDAO.setWhereQuery("refreshToken", refresh_token).findOne();
if (authRefreshToken &#61;&#61; null)
return SsoUtil.oauthError(ErrorCodeEnum.INVALID_GRANT);
// 如果 Refresh Token 已经失效&#xff0c;则需要重新生成
if (SsoUtil.checkIfExpire(authRefreshToken))
return SsoUtil.oauthError(ErrorCodeEnum.EXPIRED_TOKEN);
// 获取存储的 Access Token
AccessToken authAccessToken &#61; AcessTokenDAO.findById(authRefreshToken.getTokenId());
// 获取对应的客户端信息
ClientDetails savedClientDetails &#61; ClientDetailDAO.findById(authAccessToken.getClientId());
// 获取对应的用户信息
User user &#61; UserCommonDAO.UserDAO.findById(authAccessToken.getUserId());
// 新的过期时间
Long expiresIn &#61; LocalDateUtils.dayToSecond(ExpireEnum.ACCESS_TOKEN.getTime());
// 生成新的 Access Token
String newAccessTokenStr &#61; authService.createAccessToken(user, savedClientDetails, authAccessToken.getGrantType(), authAccessToken.getScope(), expiresIn);
IssueToken token &#61; new IssueToken(); // 返回数据
token.setAccess_token(newAccessTokenStr);
token.setRefresh_token(refresh_token);
token.setExpires_in(expiresIn);
token.setScope(authAccessToken.getScope());
return JsonHelper.toJson(token);

有些厂家不是这么 RefreshToken 的&#xff0c;它是使用基本认证的方式验证客户端身份&#xff0c;如 Authorization: Basic $Base64.encode(clientId&#43;":"&#43;clientSecret)。可见它只需要客户端信息&#xff0c;不需要用户信息。

AccesToken 和 RefreshToken 怎么用呢&#xff1f;这就要看我们 SSO Client 如何调用了&#xff0c;——下篇文章再为大家介绍。


小结

SSO 中心没有想象中的难&#xff0c;当然还有其他周边的问题&#xff0c;如安全性的问题&#xff0c;或者用户权限那部分&#xff0c;会越做越复杂的。不管怎么样只要方向路线正确&#xff0c;那么干就是了&#xff01;

推荐参考文章


  • OAuth2.0协议入门 ——非常不错&#xff0c;我也是参考其代码实现&#xff0c;它教会了我许多&#xff01;
  • SSO 开源实现 Kisso
  • 基于 OAuth 2 的 smart-sso
  • XXL-SSO
  • IAM&#xff1a;MaxKey 国内开源IAM第一品牌
  • 旧帖《新浪微博如何实现 SSO 的分析》
  • 单点登录跨域iframe互相通信方案

推荐阅读
  • 优化后的标题:深入探讨网关安全:将微服务升级为OAuth2资源服务器的最佳实践
    本文深入探讨了如何将微服务升级为OAuth2资源服务器,以订单服务为例,详细介绍了在POM文件中添加 `spring-cloud-starter-oauth2` 依赖,并配置Spring Security以实现对微服务的保护。通过这一过程,不仅增强了系统的安全性,还提高了资源访问的可控性和灵活性。文章还讨论了最佳实践,包括如何配置OAuth2客户端和资源服务器,以及如何处理常见的安全问题和错误。 ... [详细]
  • Python 实战:异步爬虫(协程技术)与分布式爬虫(多进程应用)深入解析
    本文将深入探讨 Python 异步爬虫和分布式爬虫的技术细节,重点介绍协程技术和多进程应用在爬虫开发中的实际应用。通过对比多进程和协程的工作原理,帮助读者理解两者在性能和资源利用上的差异,从而在实际项目中做出更合适的选择。文章还将结合具体案例,展示如何高效地实现异步和分布式爬虫,以提升数据抓取的效率和稳定性。 ... [详细]
  • 微信公众号推送模板40036问题
    返回码错误码描述说明40001invalidcredential不合法的调用凭证40002invalidgrant_type不合法的grant_type40003invalidop ... [详细]
  • 在PHP中如何正确调用JavaScript变量及定义PHP变量的方法详解 ... [详细]
  • 技术分享:使用 Flask、AngularJS 和 Jinja2 构建高效前后端交互系统
    技术分享:使用 Flask、AngularJS 和 Jinja2 构建高效前后端交互系统 ... [详细]
  • Python应用实例大揭秘:七大令人惊叹的高阶技巧展示
    2020年,Python无疑成为了最炙手可热的编程语言,其影响力已远远超出程序员的范畴。从初学者到资深从业者,甚至小学生,都在纷纷加入Python的学习热潮中。凭借其低门槛、易上手和强大的功能,Python正逐渐成为各行业不可或缺的工具。本文将揭示七个令人惊叹的Python高级应用技巧,帮助读者进一步提升编程水平。 ... [详细]
  • DVWA学习笔记系列:深入理解CSRF攻击机制
    DVWA学习笔记系列:深入理解CSRF攻击机制 ... [详细]
  • 【实例简介】本文详细介绍了如何在PHP中实现微信支付的退款功能,并提供了订单创建类的完整代码及调用示例。在配置过程中,需确保正确设置相关参数,特别是证书路径应根据项目实际情况进行调整。为了保证系统的安全性,存放证书的目录需要设置为可读权限。值得注意的是,普通支付操作无需证书,但在执行退款操作时必须提供证书。此外,本文还对常见的错误处理和调试技巧进行了说明,帮助开发者快速定位和解决问题。 ... [详细]
  • Web开发框架概览:Java与JavaScript技术及框架综述
    Web开发涉及服务器端和客户端的协同工作。在服务器端,Java是一种优秀的编程语言,适用于构建各种功能模块,如通过Servlet实现特定服务。客户端则主要依赖HTML进行内容展示,同时借助JavaScript增强交互性和动态效果。此外,现代Web开发还广泛使用各种框架和库,如Spring Boot、React和Vue.js,以提高开发效率和应用性能。 ... [详细]
  • 深入探索HTTP协议的学习与实践
    在初次访问某个网站时,由于本地没有缓存,服务器会返回一个200状态码的响应,并在响应头中设置Etag和Last-Modified等缓存控制字段。这些字段用于后续请求时验证资源是否已更新,从而提高页面加载速度和减少带宽消耗。本文将深入探讨HTTP缓存机制及其在实际应用中的优化策略,帮助读者更好地理解和运用HTTP协议。 ... [详细]
  • 投融资周报 | Circle 达成 4 亿美元融资协议,唯一艺术平台 A 轮融资超千万美元 ... [详细]
  • 如何高效启动大数据应用之旅?
    在前一篇文章中,我探讨了大数据的定义及其与数据挖掘的区别。本文将重点介绍如何高效启动大数据应用项目,涵盖关键步骤和最佳实践,帮助读者快速踏上大数据之旅。 ... [详细]
  • 在过去,我曾使用过自建MySQL服务器中的MyISAM和InnoDB存储引擎(也曾尝试过Memory引擎)。今年初,我开始转向阿里云的关系型数据库服务,并深入研究了其高效的压缩存储引擎TokuDB。TokuDB在数据压缩和处理大规模数据集方面表现出色,显著提升了存储效率和查询性能。通过实际应用,我发现TokuDB不仅能够有效减少存储成本,还能显著提高数据处理速度,特别适用于高并发和大数据量的场景。 ... [详细]
  • 深入解析 Vue 中的 Axios 请求库
    本文深入探讨了 Vue 中的 Axios 请求库,详细解析了其核心功能与使用方法。Axios 是一个基于 Promise 的 HTTP 客户端,支持浏览器和 Node.js 环境。文章首先介绍了 Axios 的基本概念,随后通过具体示例展示了如何在 Vue 项目中集成和使用 Axios 进行数据请求。无论你是初学者还是有经验的开发者,本文都能为你解决 Vue.js 相关问题提供有价值的参考。 ... [详细]
  • 如何在PHP中嵌入JavaScript(2023年最新实践指南) ... [详细]
author-avatar
zhang时代小窝
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有